Skip to content

feat(ui,api): edit model predictions and save versioned results - #136

Open
Caleb Robinson (calebrob6) wants to merge 10 commits into
mainfrom
calebrob6/feat/prediction-editing
Open

feat(ui,api): edit model predictions and save versioned results#136
Caleb Robinson (calebrob6) wants to merge 10 commits into
mainfrom
calebrob6/feat/prediction-editing

Conversation

@calebrob6

@calebrob6 Caleb Robinson (calebrob6) commented Aug 21, 2026

Copy link
Copy Markdown
Member

Analysts can correct a model's predicted building damage, save the result as a numbered version, and then choose which version they view, download, and report on. Both HASTE workflows produce the same kind of result, so the viewer and the reports treat them identically.

What changed

Editing happens inside the existing View Results page. Press the pencil button, or E, and the page switches to edit mode over the same map. There is no separate editor screen and no per-row Edit button, which also removes the logic that used to disable that button when footprints were missing.

Pick a class from the panel, then click a footprint to apply it. Ctrl+drag paints every building in a box, right-click restores one to the model's own call, and Enter applies the class to the selected building so the arrow-key review loop never needs the mouse. Trained-inference models also get damage and unknown sliders, which recolour the map in the browser without a server round-trip. Embedding models have no sliders, because their damage column is a 0/1 copy of the predicted class and a slider over it would only ever produce the same two groups.

Saving writes a new numbered version. Model.gpkgUrl still points at the raw prediction and versions accumulate in Model.editedPredictions, so the model's own output is never overwritten.

A dropdown on the results page switches the map between the raw output and any saved version. Downloads go through GetModelArtifact so the filename records which one you got, for example building_predictions_5553_v1.gpkg. The model rows ask the same question: when a model has saved edits, the download action opens a small dialog instead of quietly handing back the raw file. The Validation and Assessment modals each gained a "Report on" dropdown that defaults to the newest saved version.

The embedding workflow gained a View Results entry point. It has no raw model prediction layer, but everything else matches.

Why parts of this were harder than they look

!!gpkgUrl could not gate the embedding workflow. The Interactive Labeler's "Clear labels" action PUTs predictions: [], which writes a valid GeoPackage and sets gpkgUrl, so a cleared model looked exactly like a finished one. PutBuildingPredictions now stores predictedBuildingCount and predictedAt, which it already computed and returned but never persisted. One helper, model_readiness, holds the rule so the results button, the embedding row, and the publisher cannot drift apart again.

Nothing could deliver a full prediction set to the browser. GetBuildingFootprintsGeoJSON returns a random sample capped at 2000 rows, and vector tiles existed only for the embedding workflow. Trained-model predictions reached the UI as a pre-coloured raster and nothing else, which is why an embedding model had no viewer at all. Reads now go through footprint PMTiles plus a columnar attribute sidecar, and the browser colours each building through map feature-state.

Tiling cannot run in the API. tippecanoe ships only in the training image, so preparation is a queued job on a new prediction-edit-prep-queue. Layer footprints are tiled once at layer creation and shared by every model on that layer. The embedding workflow already tiles the same footprints with the same row-index ids, so those tiles are reused rather than rebuilt, and only the attribute sidecar is generated on first open.

Row order is load-bearing. Predictions join to Overture ids positionally against the footprints GeoPackage. The writer preserves row order exactly and now emits an explicit overture_id column. A count mismatch raises instead of silently misaligning, which would have mis-coloured buildings with no visible symptom.

Each version needs its own sidecar. The attribute sidecar was keyed per model, so it always described the raw predictions and switching versions had nothing to render. A per-version sidecar is now written in the same call as the version's GeoPackage, and older versions are backfilled by the same queued job.

Assessment counted the model's score, not the analyst's call. apply_edits rewrites damaged and edited_class but deliberately leaves damage_pct_0m at whatever the model predicted. Assessment counted from that score, so every version of a model reported identical numbers: model 5553 reported 740 predicted-damaged buildings even when you picked the version whose own sidecar says 770. It now reads edited_class when a row has one. No continuous score sits behind a human decision, so edited rows come through as 0.0/1.0, and an edited version's precision-recall curve is a single operating point rather than a sweep. Raw predictions are unaffected.

Feature-state is per renderer. The swipe map is two Azure Maps instances, and feature-state has to be cleared before the source is removed because the source id is reused. This class of bug shipped twice during the build.

Incidental fixes

InteractiveLabeler registered its own pmtiles protocol handler. addProtocol keeps one handler per scheme, so a second registration would have silently broken the labeler's tiles. Both now share a singleton in ui/src/util/pmtiles.js.

Double-click zoom is off while edit mode is on. Labelling two neighbouring buildings quickly registers as a double-click, and the map zoomed out from under the analyst mid-edit. Panning and scroll zoom still work.

The edit panel's scroll container is a flex column, so its children were free to shrink once the content grew taller than the panel. Buttons clipped the top and bottom of their own labels instead of the column scrolling.

The opacity, contrast, hue and saturation panel is gone from the results page, along with the state and handlers that only fed it. The layers keep the defaults the panel's reset button applied. The labeling tool has its own separate copy of those sliders, untouched.

Verification

Check Result
hatch run test:pytest 690 passed, 47 failed
UI tests (node --test) 187 passed
UI vite build exit 0
ESLint matches main's baseline; 0 problems in new files
black / isort / flake8 clean

All 47 Python failures also fail on main. Forty-six come from the hastegeo[planetary-computer] extra missing in the local conda environment, which CI installs, and the last is the pre-existing test_artifacts.py::test_zip. The failure sets were diffed against main rather than compared by count.

Version selection, downloads and the backfill were exercised against the local docker stack. Model 5553 reports 740 predicted-damaged buildings on the raw output and 770 on v1. Model 0448, which has six saved edits, moves from 2601 to 2595. Both match the per-version sidecars. Unknown versions return 404 on the artifact and report routes, and re-running preparation returns queued: false.

Known limitations

There is no 409 on concurrent saves. next_version plus save is a read-modify-write with no optimistic concurrency, so two simultaneous saves could collide. The UI already handles a 409 if one is added later.

There are no browser tests. This repo has no Playwright config, so the map interactions, the version dropdown, dark-mode contrast and the prepare-to-ready transition were checked through the API and the rendered bundle rather than a real browser. The 1100px breakpoint where the version bar drops to its own row is an estimate and worth a look at a real window width.

The selected version is not in the URL, so a version cannot be shared or bookmarked.

Bicep app-setting parity is skipped for the new queue. Config resolves the default and the Functions host creates the queue on first write, and editing functions.bicep without regenerating infra/main.json would cause drift.

API-level integration tests are absent. Coverage sits at the processor and model level; api/hastefuncapi/tests/ currently holds only test_publishing_routes.py.

New routes

GetPredictionEditSession, PutEditedPredictions, GetEditedPredictionVersions and PutPreparePredictionTilesQueueMessage. GetVisualizerResults and GetModelArtifact take an optional version, as do GetValidationReport and GetAssessmentReport.

Spec

spec/features/prediction-editing/, plus ADR 0005-versioned-derived-prediction-artifacts.md, the first artifact-versioning concept in this codebase. The spec was reconciled against the final implementation, including the routes that changed shape during the build.

@calebrob6
Caleb Robinson (calebrob6) requested a review from a team as a code owner August 21, 2026 06:31
Base automatically changed from feat/labeling-controls-and-hotkeys to main August 21, 2026 16:06
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.30rc11
  • training_image_tag: 1.0.30rc11
  • imageprep_image_tag: 1.0.30rc11
  • wheel: hastegeo-1.0.30rc11-py3-none-any.whl

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.30rc13
  • training_image_tag: 1.0.30rc13
  • imageprep_image_tag: 1.0.30rc13
  • wheel: hastegeo-1.0.30rc13-py3-none-any.whl

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a full prediction-editing workflow for trained and embedding models, backed by queued artifact preparation and immutable versioned GeoPackages.

Changes:

  • Adds Prediction Editor UI, classification controls, swipe comparison, polling, and version history.
  • Adds API, queue, processor, and geospatial workflows for preparation and saving.
  • Adds specifications and comprehensive unit coverage.

Reviewed changes

Copilot reviewed 45 out of 47 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
api/hastefuncapi/function_app.py Adds editing and artifact APIs.
api/hastefuncqueues/function_app.py Adds preparation queue worker.
docker/data-init/upload_data.py Initializes new artifact metadata.
docker/docker-compose.yml Configures preparation queue.
docs/api/hastefuncapi.md Documents new endpoints.
hastelib/logs/embedding_friendly.log Updates embedding log fixture.
hastelib/logs/prediction_tiles_friendly.log Adds preparation log fixture.
hastelib/pyproject.toml Registers workflow tooling.
hastelib/src/hastegeo/core/config.py Defines queue and artifact settings.
hastelib/src/hastegeo/core/models/predictions.py Defines request validation models.
hastelib/src/hastegeo/core/models/projects.py Adds prediction-version metadata.
hastelib/src/hastegeo/core/processors/imagery.py Schedules footprint tiling.
hastelib/src/hastegeo/core/processors/prediction_edits.py Applies and versions edits.
hastelib/src/hastegeo/core/processors/prediction_tiles.py Orchestrates artifact preparation.
hastelib/src/hastegeo/core/utils/predictions.py Normalizes prediction schemas.
hastelib/src/hastegeo/workflows/prepare_prediction_tiles.py Builds tiles and sidecars.
hastelib/tests/core/models/__init__.py Initializes model tests.
hastelib/tests/core/models/test_prediction_wire_models.py Tests request validation.
hastelib/tests/core/processors/test_imagery_footprint_tiles.py Tests eager tiling scheduling.
hastelib/tests/core/processors/test_prediction_edits.py Tests edit persistence.
hastelib/tests/core/processors/test_prediction_tiles.py Tests preparation orchestration.
hastelib/tests/core/processors/test_prediction_tiles_layer.py Tests layer-only preparation.
hastelib/tests/core/processors/test_prediction_tiles_request.py Tests preparation requests.
hastelib/tests/core/utils/test_predictions.py Tests schema normalization.
hastelib/tests/workflows/test_prepare_prediction_tiles.py Tests geospatial preparation.
local.settings.example.jsonc Documents local queue setting.
spec/architecture/decisions/0005-versioned-derived-prediction-artifacts.md Records versioning architecture.
spec/features/prediction-editing/README.md Summarizes the feature.
spec/features/prediction-editing/data-model.md Documents metadata changes.
spec/features/prediction-editing/design.md Defines technical design.
spec/features/prediction-editing/impact-analysis.md Assesses risks and impact.
spec/features/prediction-editing/plan.md Tracks implementation work.
spec/features/prediction-editing/rollout.md Defines rollout strategy.
spec/features/prediction-editing/test-plan.md Defines validation coverage.
spec/features/prediction-editing/user-stories.md Defines user requirements.
ui/src/Components/AppBody.jsx Registers editor route.
ui/src/Components/InteractiveLabeler/InteractiveLabeler.jsx Shares PMTiles protocol.
ui/src/Components/PredictionEditor/PredictionEditor.jsx Implements the editor screen.
ui/src/Components/PredictionEditor/PredictionEditorRightPanel.jsx Implements editing controls.
ui/src/Components/PredictionEditor/predictionClassify.js Implements classification logic.
ui/src/Components/PredictionEditor/predictionClassify.test.js Tests UI helper behavior.
ui/src/Components/PredictionEditor/predictionPrep.js Implements polling state machine.
ui/src/Components/PredictionEditor/predictionSwipe.js Implements swipe decisions.
ui/src/Components/ProjectManagement/EmbeddingModelRow.jsx Adds embedding Edit action.
ui/src/Components/ProjectManagement/ModelResultsButton.jsx Adds inference Edit action.
ui/src/Components/keyboardShortcuts.js Extends shortcut handling.
ui/src/util/pmtiles.js Provides shared PMTiles plumbing.
Suppressed comments (3)

api/hastefuncapi/function_app.py:2964

  • The supplied image layer is loaded independently of the model and never checked against model_data.imageLayerId. Because predictions and footprints are joined positionally, a valid same-project layer ID can make this session expose unrelated geometry as editable model predictions (and equal row counts will not catch it). Reject a mismatched model/layer pair before reading the GeoPackages.
        image_layer_data = await asyncio.to_thread(

api/hastefuncapi/function_app.py:3104

  • This preparation route accepts any same-project image layer without verifying that it is the model's layer. A mismatched request can build and persist an attribute sidecar by position against unrelated footprints, corrupting the editor's model-to-building mapping. Validate model_data.imageLayerId == prep_request.imageLayerId before queueing.
        image_layer_data = await asyncio.to_thread(

api/hastefuncapi/function_app.py:3230

  • The save path also trusts an independently supplied image layer. If another layer happens to have the same footprint count, edits are written with those unrelated Overture IDs and pass the row-count guard. Reject the request unless edit_request.imageLayerId is the layer recorded on the model before downloading either artifact.
        image_layer_data = await asyncio.to_thread(

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

model nor the layer already has a usable archive.
"""
needs_pmtiles = not bool(resolve_tiles_url(model, image_layer))
needs_attrs = not bool(model.predictionAttrsUrl)
Comment thread api/hastefuncapi/function_app.py Outdated
Comment on lines +1474 to +1480
image_layer_id = req.params.get(
"imageLayerId"
) or model_document.get("imageLayerId")
if not image_layer_id or not _GUID_RE.match(str(image_layer_id)):
return _bad_request(
"Invalid or missing parameter: imageLayerId"
)
Comment on lines +253 to +257
if (
self.image_data.status
== self.config.get_status_types().COMPLETED.value
):
self._enqueue_footprint_tiles()
Comment on lines +124 to +140
def _detect_flavor(
layer_name: Optional[str],
field_names: List[str],
damage_values: List[float],
) -> str:
"""Classify which producer wrote a prediction layer.

A ``predictions`` layer carrying an ``area`` column is the
interactive labeler's output. Failing that, a ``damage_pct_0m``
column that only ever holds 0.0 or 1.0 is the labeler's degenerate
copy of ``damaged`` rather than a real damage fraction.
"""
if layer_name == EMBEDDING_LAYER_NAME and AREA_FIELD in field_names:
return EMBEDDING_FLAVOR
if damage_values and all(v in (0.0, 1.0) for v in damage_values):
return EMBEDDING_FLAVOR
return INFERENCE_FLAVOR
Comment on lines +925 to +930
except Exception as e:
logger.error(
"PreparePredictionTilesQueueTrigger: Error processing queue "
f"message: {e}\n{traceback.format_exc()}",
stack_info=True,
)
@calebrob6
Caleb Robinson (calebrob6) marked this pull request as draft August 21, 2026 17:55
@calebrob6
Caleb Robinson (calebrob6) force-pushed the calebrob6/feat/prediction-editing branch from e718641 to 7a7404c Compare August 22, 2026 22:40
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.35rc2
  • training_image_tag: 1.0.35rc2
  • imageprep_image_tag: 1.0.35rc2
  • wheel: hastegeo-1.0.35rc2-py3-none-any.whl

@calebrob6
Caleb Robinson (calebrob6) force-pushed the calebrob6/feat/prediction-editing branch from 2e47880 to 10e428c Compare August 25, 2026 19:01
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.38rc1
  • training_image_tag: 1.0.38rc1
  • imageprep_image_tag: 1.0.38rc1
  • wheel: hastegeo-1.0.38rc1-py3-none-any.whl

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.38rc2
  • training_image_tag: 1.0.38rc2
  • imageprep_image_tag: 1.0.38rc2
  • wheel: hastegeo-1.0.38rc2-py3-none-any.whl

Every model row in both workflows gains an Edit button that opens a new
screen showing all predicted building footprints. An analyst can
reclassify buildings and, for trained-inference models, re-threshold on
damage_pct_0m, then save the result as a new version of the prediction
geopackage.

The button stays disabled until a full prediction set exists:
inferenceStatus == "Processed" for a trained model, and a non-zero
predictedBuildingCount for the embedding workflow. That count is new —
"Clear labels" also writes a geopackage and sets gpkgUrl, so the old
!!gpkgUrl check could not tell a completed pass from a cleared one.

Reads go through PMTiles plus a columnar attribute sidecar, prepared by
a queued job that runs in the training image because tippecanoe only
exists there. Nothing before this could deliver a full prediction set to
the browser: the GeoJSON route returns a random sample capped at 2000,
and vector tiles existed only for the embedding workflow. The threshold
slider recolours from the sidecar in the browser, so it costs no server
round-trip.

Writes never touch the raw output. The client sends a threshold and a
sparse override list; the server re-reads the source geopackage, applies
threshold then overrides, and writes a new numbered version. Model.gpkgUrl
still points at the raw prediction, and each version is recorded in
Model.editedPredictions. Row order is preserved exactly, because two
downstream consumers join predictions to Overture ids positionally; an
explicit overture_id column is now written alongside.

The two producers disagree on schema — trained inference writes a
continuous damage_pct_0m in the raster CRS, while the embedding endpoint
writes layer "predictions" with a degenerate 0/1 copy — so a normalising
reader hides the difference and thresholding is offered only where it is
meaningful.

Also fixes a latent conflict: the Interactive Labeler registered its own
pmtiles protocol handler, and addProtocol keeps one handler per scheme,
so a second registration from the editor would have silently broken its
tiles. Both now share a singleton.

Specs and an ADR for versioned derived artifacts are included, and the
stale API reference is brought up to date.
Preparing an embedding model for editing failed, and would have wasted
a multi-gigabyte container job even once it worked.

Reuse the tiles we already have. The embedding workflow tiles the same
footprints from the same PMTiles archive, keyed on the same integer
row-index id, and records it as Model.pmtilesUrl. needs_preparation only
looked at ImageLayer.footprintPmtilesUrl, so it rebuilt a byte-for-byte
equivalent archive. Preparation now resolves the model's own archive
first, and GetModelArtifact serves footprint_pmtiles from it, which also
means that kind no longer needs an imageLayerId for embedding models.

Stop importing the queue SDK inside the training image. The workflow
stored its outputs through ArtifactProcessor, which also drives zip jobs
and therefore imports azure.storage.queue — a package the training image
does not install, so the job died with ModuleNotFoundError after all the
real work was done. It now talks to UnifiedArtifactStorage directly,
constructed exactly as ArtifactProcessor does; putting bytes in blob
storage is all this workflow ever needed.

Verified against a real embedding model in the local stack: preparation
reports tilesReady immediately, the job reaches Processed, and the
sidecar serves 1496 buildings with every column the same length.
Build the layer's footprint PMTiles as part of image-layer creation, so
a trained-inference model opens straight into the editor instead of
waiting on a tiling job. Tippecanoe lives only in the training image —
not in imageryprep and not in the Functions app — so the tiling itself
stays a queued job; imagery prep simply enqueues it once the footprints
are cached.

That needed a layer-only mode for the prep job. modelId and the
predictions URL are now optional: without them the job builds the
PMTiles, skips the sidecar, and records status on the image layer rather
than the model. The message schema is unchanged, so the queue contract
stays backward compatible, and the on-demand path still tiles older
layers when someone opens the editor. Layer-time status writes patch
only the tiling fields, because imagery prep is writing the same
document around the same time. Enqueue failures are logged and
swallowed: tiles are an optimisation, and the editor can still build
them on demand.

Add a swipe comparison to the editor: pre-event against post-event, or
basemap against post-event when the layer has no pre-event imagery.
atlas.SwipeMap reveals the PRIMARY map on the left and clips the
SECONDARY, so the comparison map is primary and the editor map is
secondary — moving the divider left uncovers more of the editing map.
A recent PR shipped that description inverted, so a test pins the
wording. Editing is wired to both panes and feature-state and paint
expressions are mirrored across renderers; otherwise half the map is
inert and the far side draws every footprint unlabeled. A/S/D move the
divider, consistent with the other map views.

Verified in the local stack: a layer-only job runs tippecanoe for real
and lands footprintPmtilesUrl, and the tiles serve as valid PMTiles.
Review asked for prediction editing to be a mode inside the existing
View Results page rather than a screen of its own. Doing that surfaced
why the two workflows had never really shared a results story.

The viewer was raster-only: a pre-coloured visualizer COG plus a raw
predictions raster, both produced solely by the train/infer workflow.
The embedding workflow makes no rasters at all, so pointing it at the
viewer would have shown an empty map — and no code path anywhere
rendered per-building predictions, for either workflow. So the viewer
now draws predicted building footprints as vectors from the footprint
tiles and the attribute sidecar, artifacts both workflows already
produce, and the rasters become optional layers that simply do not
appear when a model has none. That is what lets both workflows produce
mostly the same results and be handled the same way.

Editing rides on that same layer. A pencil beside Back, or E, turns the
footprints interactive: click to set a class, ctrl+drag to box-select,
right-click to clear an override back to the derived class, with the
threshold slider shown only where re-thresholding means anything — the
embedding workflow's damage fraction is a degenerate 0/1 copy. Leaving
edit mode restores the read-only view and confirms before discarding
unsaved work. The standalone screen, its route and both model-row Edit
buttons are gone, and with them the logic that existed only to disable
those buttons.

The embedding row gains the View Results it never had.

Three places disagreed about whether a model has results, so there is
now one rule in hastegeo, surfaced as predictionsReady, and publishing
uses it too rather than keeping a fourth copy. Reads resolve their
source through resolve_prediction_source, so an analyst's edits finally
reach the reports instead of being written and then ignored; the newest
version wins and an explicit version can be requested, with no mutable
"active version" pointer, which ADR-0005 deliberately avoided.

Guards the null rasters this makes possible: four raster loaders
dereferenced fields that are now absent for embedding models, and one
older payload shape handed them a tile template with an empty url that
would have quietly served 404s rather than failing.
Saving an edit produced a version nobody could reach. The history was
rendered only inside the edit panel, so a read-only viewer could not see
that versions existed, nothing ever refetched to draw a different one,
and every download button fetched the raw output regardless.

Switching was not simply unwired. The attribute sidecar the viewer draws
from is keyed per model, not per version, so it always described the raw
predictions; saving wrote a GeoPackage but no sidecar. There was nothing
version-specific to render. Every saved version now gets its own sidecar,
written in the same call as its GeoPackage so the two cannot drift, which
makes drawing a version the same code path as drawing the raw output with
a different URL. The builder moves out of the training workflow into a
shared module so the API can produce one without importing an image it
has no business importing.

Versions saved before this are backfilled by the tiles job, idempotently
and skipping any that already have a sidecar. A version still waiting is
offered as disabled with the reason, and the raw sidecar is deliberately
never substituted for a missing one — quietly drawing the wrong classes
is worse than drawing nothing.

The selector sits in the read-only view, and switching rebuilds the
source, layers and feature-state on both swipe panes. Feature-state is
per-renderer and the source id is reused, so state is cleared before the
source is removed; otherwise one pane keeps the previous version's
colours, which is how this repo has twice shipped a half-updated map.

Selection moves the map only — the reports keep reading the newest
version — so the screen says when the two diverge rather than leaving it
to be discovered.

Downloads go through the artifact route, which already handles auth and
ranges, instead of rewriting storage URLs.

Also fixes a data-loss hazard the switching exposed: saves always derive
from the raw GeoPackage, so saving on top of a loaded version had to
resend that version's classes or it would silently discard the edits it
was built on.
…k zoom

Editing a footprint's class went through two controls that overlapped: a
"Clicking a footprint" radio group whose default was a cycle mode, and a
separate row of buttons that set the *selected* building's class. The two
were coupled — pressing 1/2/3, or using the buttons, silently changed the
click mode as a side effect — so what a click would do depended on state
the analyst had changed several actions ago.

Replace both with one class picker. The picked class is what every edit
gesture applies: left-click a footprint, or Ctrl+drag to box-select many.
Right-click still undoes an edit. The cycle mode is gone, along with
cycleClass() and the branch that advanced each building from its own class
during a box-select.

Picking a class no longer edits anything on its own. Relabelling whatever
happened to be selected, because the analyst pressed a number key to line
up their next click, is the surprise this mode should not have; Enter is
now the explicit "apply to the selected building" for the arrow-key review
flow, and is listed in the shortcut help.

normalizeEditClass() guards the one-way door: every path that writes an
override now funnels through it, so a stale value (the old "cycle"
sentinel, say) cannot reach the overrides map and, from there, a saved
GeoPackage.

Also disable Azure Maps' double-click zoom for as long as edit mode is on.
Labelling two neighbouring buildings quickly registers as a double-click,
and the map zoomed out from under the analyst mid-edit. Panning and scroll
zoom are untouched, and the gesture is restored on exit and on unmount.
@calebrob6
Caleb Robinson (calebrob6) force-pushed the calebrob6/feat/prediction-editing branch from ef36731 to a26c74d Compare August 31, 2026 17:44
The model rows downloaded the raw GeoPackage unconditionally, and the two
report modals took whatever the server picked. Once a model has saved
edits that is a silent choice made on the analyst's behalf: they correct
predictions, download the file, and get the model's uncorrected output
back with a filename that gives no hint which one it is.

Both places now ask.

Download: when a model has saved versions the row opens a small dialog to
pick one, defaulting to the newest, and streams it through
GetModelArtifact so the filename carries the version (_v1). A model
nobody has edited has nothing to choose, so it downloads directly as
before and never sees the dialog.

Reports: Validation and Assessment each get a "Report on" dropdown,
defaulting to the newest saved edit — the same rule the server applies
when no version is passed, so the default is what these modals already
showed. Both endpoints already accepted `version`; this only gives the
UI a way to send it.

predictionSourceOptions is deliberately not versionSelectorOptions. That
one disables a version whose attribute sidecar has not been backfilled,
because the map cannot colour buildings without it. A download reads the
GeoPackage, where the sidecar is irrelevant, so reusing the map's rule
would have hidden perfectly good files behind "preparing…". A test pins
the difference.

Known gap, unchanged by this commit: Validation reads the `damaged`
column, which apply_edits rewrites, so it follows the selection.
Assessment counts from `damage_pct_0m`, which apply_edits leaves at the
model's original score, so its headline counts do not move between
versions yet.
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.39rc5
  • training_image_tag: 1.0.39rc5
  • imageprep_image_tag: 1.0.39rc5
  • wheel: hastegeo-1.0.39rc5-py3-none-any.whl

Assessment read damage_pct_0m, which apply_edits deliberately leaves at
whatever the model predicted, so its headline counts were identical for
every version of a model. Picking "Version 1" in the new report dropdown
changed nothing on screen: model 5553 reported 740 predicted-damaged
buildings whether or not you selected the version whose own sidecar says
770.

build_assessment_inputs_from_gpkgs now reads edited_class when a row
carries one and falls back to the score otherwise, so raw predictions
behave exactly as before. "Unknown" is recorded as full unknown coverage,
which is what already excludes a building from the known population and
from the damaged count.

Verified against the dev stack: model 5553 now reports 740 for the raw
output and 770 for v1; model 0448, which has six edits, moves 2601 -> 2595.
Both match the per-version attribute sidecars.

There is no continuous score behind a human decision, so edited rows come
through as 0.0/1.0 and an edited version's precision-recall curve is a
single binary operating point rather than a sweep. That is a real change
in what the curve means, and it is the honest one: the alternative was
ranking buildings by a score the analyst has already overruled.
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.39rc6
  • training_image_tag: 1.0.39rc6
  • imageprep_image_tag: 1.0.39rc6
  • wheel: hastegeo-1.0.39rc6-py3-none-any.whl

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 65 out of 77 changed files in this pull request and generated 3 comments.

Comment on lines +127 to 131
if (hasPredictionVersionChoice(model.editedPredictions)) {
setShowDownloadPredictions(true);
return;
}
handleDownload(model.gpkgUrl);
Comment on lines +816 to +817
const result = await footprints.save();
setDialog(
Comment on lines +2378 to +2383
switching versions only changes a URL. ``predictionVersionIsLatest``
says whether that version is the newest saved one: version selection
moves the map only, while the Assessment/Validation reports always
read the newest version, and the UI has to be able to say when the
two diverge. A version whose sidecar has not been built yet reports
``predictionsReadiness.attrsReady = false`` (reason ``preparing``)
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.39rc7
  • training_image_tag: 1.0.39rc7
  • imageprep_image_tag: 1.0.39rc7
  • wheel: hastegeo-1.0.39rc7-py3-none-any.whl

…tons

The version selector sat at top: 66px, a row of its own below the
Back/Edit controls, leaving an obvious band of empty map between the top
of the view and the first control. It now shares the top row with the
corner blocks. Below 1100px a 560px centre column plus both corners stops
fitting side by side, so it drops back to its own row there rather than
sliding under them — the corners are z-index 1000 and it is 950.

The edit panel's scroll container is a flex column, so every block inside
it was free to shrink once the content was taller than the panel. Buttons
lost the top and bottom of their own labels instead of the column simply
scrolling; "No manual edits" was the clearest case. Direct children of
the scroll area and of the button stacks no longer shrink.
Footprint geometry belongs to the image layer: every model trained on a
layer draws the same buildings. Two jobs were tiling it anyway. Imagery
prep builds the layer archive when the footprints are cached, and the
embedding job built its own per-model copy on top of that.

The two archives were the same file. Both ran tippecanoe with the same
layer name, the same --use-attribute-for-id=id, the same -y id -y
overture_id, and the same zoom range, over the same rows in the same
order (embed_buildings reads the layer footprints, reset_index, and keeps
every row including the ones with no valid tokens). So an embedding layer
paid for a byte-for-byte duplicate.

The embedding workflow no longer tiles. tippecanoe and subprocess drop
out of it entirely, and it emits the embeddings GeoJSON, the HFTR feature
sidecar and the manifest. The feature sidecar is per model and stays
exactly as it was.

With one archive, resolve_tiles_url stops choosing between a model field
and a layer field, Model.pmtilesUrl and ArtifactTypes.BUILDING_PMTILES
retire, and GetModelArtifact loses both the "pmtiles" kind and the branch
that reused a model's archive for a layer-scoped request. The Interactive
Labeler reads kind=footprint_pmtiles instead.

Existing embedding models keep a stale pmtilesUrl in their stored
metadata. Nothing reads it, and re-tiling old layers is out of scope by
agreement.

The labeler used to refuse to start when a model had no archive. That
guard is gone with the field, so a failed tile load now raises a clear
error instead of warning to the console and leaving an empty map with no
buildings to label.
@calebrob6
Caleb Robinson (calebrob6) force-pushed the calebrob6/feat/prediction-editing branch from fec2de9 to af16bc4 Compare August 31, 2026 21:07
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.39rc8
  • training_image_tag: 1.0.39rc8
  • imageprep_image_tag: 1.0.39rc8
  • wheel: hastegeo-1.0.39rc8-py3-none-any.whl

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.39rc9
  • training_image_tag: 1.0.39rc9
  • imageprep_image_tag: 1.0.39rc9
  • wheel: hastegeo-1.0.39rc9-py3-none-any.whl

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants